package com.itheima.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * AOP概述
 * <p>
 * AOP：Aspect Oriented Programming（面向切面编程、面向方面编程），其实就是面向特定方法编程。
 * <p>
 * 场景：
 * 案例部分功能运行较慢，定位执行耗时较长的业务方法，此时需要统计每一个业务方法的执行耗时
 * 1. 获取方法运行开始时间
 * 2. 运行原始方法
 * 3. 获取方法运行结束时间,计算执行耗时
 * <p>
 * 实现：
 * 动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术，旨在管理bean对象的过程中，主要通过底层的动态代理机制，对特定的方法进行编程。
 * <p>
 * 步骤:
 * 1.导入依赖：在pom.xml中导入AOP的依赖(体现在@Aspect注解)
 * <dependency>
 * <groupId>org.springframework.boot</groupId>
 * <artifactId>spring-boot-starter-aop</artifactId>
 * </dependency>
 * <p>
 * 2.编写切面类：针对于特定方法根据业务需要进行编程
 * <p>
 * <p>
 * <p>
 * 应用场景:
 * 1.记录操作日志 2.权限控制 3.事务管理
 * <p>
 * ● 项目二神领物流中:AOP可以在Controller中用于对前端请求携带的数据进行合法性校验. 具体而言,对于使用@RequestBody注解接收的post请求请求体中的json数据,我们使用AOP面向切面编程,在切面类中给Controller中所有方法使用增强函数around包裹, 在around函数中对前端json转成的对象(DTO)进行合法性校验,不合法就抛出自定义异常,合法就继续执行原Controller中的方法.
 * <p>
 * 优势:
 * 1.代码无入侵性 2.减少重复代码 3.提高开发效率 4.维护方便
 * <p>
 * <p>
 * AOP核心概念:
 * 连接点：JoinPoint，被代理的接口中可以被AOP切面类增强的方法（暗含方法执行时的相关信息）
 * 通知(应该叫"增强")：Advice，就是对连接点进行执行前后功能增强的代码段（最终体现为一个方法）
 * 切入点：PointCut，就是被切面类管理的连接点(切面类中的方法通过切入点表达式[@Around注解]指定要管理的连接点)
 * 切面：Aspect，就是切面类中 切入点表达式+增强通知代码段 构成的一个方法（通知+切入点）
 * 目标对象：Target，就是被代理的接口的实现类
 */

/* 注意:
 * AOP面向切面编程一般只对Service层接口中的方法进行代理增强,然后Controller层中@Autowired自动装配(依赖注入)的Service层对象将是被功能增强过后的实现类对象
 */


@Component
@Aspect  /*要想不用增强,只需要把@Component和@Aspect注释掉即可*/
@Slf4j
public class TimeAspect {

    /**
     * 增强功能：统计方法运行时间
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */

    @Around("myPointCut()")//myPointCut()来自抽取的切入点表达式(见后文),@Around是环绕增强通知(见后文)
//  原始写法:@Around("execution(* com.itheima.service.DeptService.*(..))")

    //↓增强通知逻辑
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();//记录开始时间

        /*proceed v.继续,前进,行进*/
        Object proceedResult = proceedingJoinPoint.proceed();//执行切入点(被代理的方法),在此行之前就是被增强的方法执行之前要执行的增强内容,等价于@Before注解修饰的方法,同理,此行之后就是被增强的方法执行之后要执行的增强内容,等价于@After注解修饰的方法

        /**
         * 当Service层接口中的方法被代理时,Controller层中自动注入的那个userService实现类对象是原来的UserService接口的代理类对象,userService调用的也将是切面类增强后的方法
         *
         * 当proceedingJoinPoint是带参方法时,按理来说上句应当为:
         * Object[] args = proceedingJoinPoint.getArgs();
         * Object proceedResult = proceedingJoinPoint.proceed(args);
         * 但实际上不需要手动传参,SpringMVC会自动传参
         */

        long endTime = System.currentTimeMillis();//记录结束时间

        log.info("{}方法执行了{}毫秒", proceedingJoinPoint.getSignature().getName(), endTime - startTime);

        //访问http://localhost:8080/users
        //2023-04-20 06:26:31.926  INFO 17212 --- [nio-8080-exec-1] com.itheima.aspect.TimeAspect            : selectAll方法执行了342毫秒

        //访问http://localhost:8080/users/1
        //2023-04-20 06:27:21.505  INFO 17212 --- [nio-8080-exec-3] com.itheima.aspect.TimeAspect            : selectById方法执行了39毫秒

        return proceedResult;
    }


    /**
     * 增强(通知)类型:
     *
     * @Around：环绕增强，此注解标注的增强方法在目标方法前、后都被执行
     * @Before：前置增强，此注解标注的增强方法在目标方法前被执行
     * @After ：后置增强，此注解标注的增强方法在目标方法后被执行，无论是否有异常都会执行
     * @AfterReturning ： 返回后增强，此注解标注的增强方法在目标方法后被执行，有异常不会执行
     * @AfterThrowing ： 异常后增强，此注解标注的增强方法发生异常后执行
     * <p>
     * <p>
     * <p>
     * 注意事项:
     * @Around环绕增强可以通过形参ProceedingJoinPoint proceedingJoinPoint (proceedingJoinPoint是切入点,也即要被增强的方法) 获取要被增强的原始方法, 需要自己调用 proceedingJoinPoint.proceed() 来让原始方法执行，其他通知不需要考虑目标方法执行
     * <p>
     * 被增强的方法如果有返回值，那么增强方法的返回值必须指定为Object,因为增强不同的方法会返回不同类型,Object才能通用,来接收一切原始方法的返回值。
     * <p>
     * 在Spring中用JoinPoint抽象了连接点，用它可以获得方法执行时的相关信息，如目标类名、方法名、方法参数等。
     * 对于 @Around 通知，获取连接点信息只能使用  ProceedingJoinPoint
     * 对于其他四种通知，获取连接点信息只能使用 JoinPoint ，它是 ProceedingJoinPoint 的父类型
     * @Around("execution(* com.itheima.service.DeptService.*(..))")
     * public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
     * String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
     * Signature signature = joinPoint.getSignature(); //获取目标方法签名
     * String methodName = joinPoint.getSignature().getName(); //获取目标方法名
     * Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
     * <p>
     * Object res = proceedingJoinPoint.proceed(); //执行原始方法,获取返回值（环绕通知）
     * <p>
     * return res;
     * }
     * @Before("execution(* com.itheima.service.DeptService.*(..))")
     * public void before(JoinPoint joinPoint) {
     * String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
     * Signature signature = joinPoint.getSignature(); //获取目标方法签名
     * String methodName = joinPoint.getSignature().getName(); //获取目标方法名
     * Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
     * }
     */
    @Before("myPointCut()")//@After @AfterReturning @AfterThrowing
    public void bMethod() {
        log.info("我是Before前增强");
        //访问http://localhost:8080/users
        //2023-04-20 07:02:34.572  INFO 17164 --- [nio-8080-exec-1] com.itheima.aspect.TimeAspect            : 我是Before前增强
    }


    /*
     * @PointCut用于抽取切入点表达式 该注解的作用是将公共的切点表达式抽取出来，需要用到时引用该切点表达式即可。
     * <p>
     * 定义:
     * @PointCut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
     * public void pt(){}
     * //注意此处使用函数名作为切点表达式的名称
     * //private修饰  ：仅能在当前切面类中引用该表达式
     * //public修饰   ：在其他外部的切面类中也可以引用该表达式
     * 调用:
     * @Around("pt()")//调用切点表达式时需要加括号
     * public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
     * ...
     * }
     */


//    切入点表达式：描述切入点方法的一种表达式
//    作用：主要用来决定项目中的哪些方法需要被增强
//    常见形式：
//    execution(……)：根据方法的签名来匹配
//    @annotation(……) ：根据注解匹配

//    切入点表达式-execution
//    execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配，语法为：
//    execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

//    其中带 ? 的表示可以省略的部分
//访问修饰符：可省略（比如: public、protected）
//包名.类名.： 可省略
//throws 异常：可省略（注意是方法上声明抛出的异常，不是实际抛出的异常）

//    可以使用通配符描述切入点
//* ：单个独立的任意符号，可以通配任意返回值、包名、类名、方法名、任意类型的一个参数，也可以通配包、类、方法名的一部分
//    execution(* com.*.service.*.update*(*))
//.. ：多个连续的任意符号，可以通配任意层级的包，或任意类型、任意个数的参数
//    execution(* com.itheima..DeptService.*(..))

//    注意事项:根据业务需要，可以使用 且（&&）、或（||）、非（!） 来组合比较复杂的切入点表达式。

//    书写建议:
//1.所有业务方法名在命名时尽量规范，方便切入点表达式快速匹配。如：查询类方法都是 find 开头，更新类方法都是 update开头。
//2.描述切入点方法通常基于接口描述，而不是直接描述实现类，"com.itheima.service.接口名.*(..)"将可以匹配所有实现类,增强拓展性。
//3.在满足业务需要的前提下，尽量缩小切入点的匹配范围。如：包名匹配尽量不使用 ..，使用 * 匹配单个包。


    //    切入点表达式-@annotation(仅做了解)
//    @annotation 切入点表达式，用于匹配标识有特定注解的方法。
//    @annotation(com.itheima.anno.Log)
//
//    @Before ( "@annotation ( com.itheima.anno.Log)")
//    public void before(){
//        log.info ( "before ...." );
//    }
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    public void myPointCut() {
    }
    /* 第一个*意思是 返回值是任意类型
     * 第二个*意思是 com.itheima.service.impl包下的任意类
     * 第三个*意思是 类中的任意方法
     * (..)表示任意个数参数
     * */


    @Pointcut("execution(* com.itheima.service.impl.UserServiceImpl.selectById(..))")
    public void myPointCut2() {
    }

    @After("myPointCut2()")//只有访问selectById的url时才会增强
    public void aMethod() {
        log.info("我是After后增强");
    }
}


/**
 * (增强)通知顺序 [了解即可,通常不在乎执行顺序]
 * <p>
 * 当有多个切面类的增强通知都匹配到了目标方法，目标方法运行时，多个通知方法都会被执行,但是存在先后顺序
 * <p>
 * 不同切面类中，默认按照切面类的类名字母排序：
 * 目标方法前的通知方法：字母排名靠前的先执行
 * 目标方法后的通知方法：字母排名靠前的后执行
 * <p>
 * 用 @Order(数字) 加在切面类上来控制顺序
 * 目标方法前的通知方法：数字小的先执行
 * 目标方法后的通知方法：数字小的后执行
 */
